#!/usr/bin/python3

# dewinfont is copyright 2001 Simon Tatham. All rights reserved.
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
########################################################################
#
# Feb 2017 - Updated to Python3.  Added in/ex-leading.
#            ".x" instead of "01".  Other minor changes.  [mj.Jernigan]
#            Could rewrite all the byte handling in a Py3 way,
#            but why bother?
#
########################################################################
########################################################################
#
# Aug 2018 - Modifications for FabGL to generate C headers - Fabrizio Di Vittorio
#            -p and -o are no more required (outfile is the same of outfile with extension changed)
#            Added -h option to generate C header files
# Maj 2019 - Added support to variable width FabGL fonts
#
########################################################################

import sys
import re
import os
import math

# Extract bitmap font data from a Windows .FON or .FNT file.

def frombyte(s):
  #return ord(s[0])
  return s[0]
def fromword(s):
  return frombyte(s[0:1]) + 256 * frombyte(s[1:2])
def fromdword(s):
  return fromword(s[0:2]) | (fromword(s[2:4]) << 16)

def asciz(s):
  #i = string.find(s, "\0")
  i = s.find(b"\0")
  if i != -1:
    s = s[:i]
  return s

def bool(n):
  if n:
    return "yes"
  else:
    return "no"

class font:
  pass

class char:
  pass


def savefont_h(f, file):
  "Write out a .h form of an internal font description."
  fname = re.sub('[^0-9a-zA-Z]+', '_', f.facename)
  file.write("// {}\n".format(f.facename))
  file.write("// .h font description generated by dewinfont.\n")
  file.write("// Font Copyright " + f.copyright + "\n\n")
  file.write("#pragma once\n\n")
  file.write("namespace fabgl {\n\n")
  file.write("#ifdef FABGL_FONT_INCLUDE_DEFINITION\n\n")

  if next((x for x in f.chars if x.width != 0 and f.chars[0].width != x.width), None) == None:
    savefontFixed_h(f, file, fname)
  else:
    savefontVar_h(f, file, fname)


def savefontFixed_h(f, file, fname):

  width = f.chars[0].width

  file.write("static const uint8_t FONT_{}_DATA[] = {{\n".format(fname))
  for i in range(256):
    file.write("  ")
    for j in range(f.height):
      if f.chars[i].width == 0:
        v = 0
      else:
        v = f.chars[i].data[j]
      if width <= 8:
        file.write(" 0x{:02x},".format(v << (8 - width)))
      elif width <= 16:
        v = v << (16 - width)
        file.write(" 0x{:02x},".format(v >> 8))
        file.write(" 0x{:02x},".format(v & 0xFF))
      elif width <= 24:
        v = v << (24 - width)
        file.write(" 0x{:02x},".format((v >> 16) & 0xFF))
        file.write(" 0x{:02x},".format((v >> 8) & 0xFF))
        file.write(" 0x{:02x},".format(v & 0xFF))
      else:
        sys.stderr.write("Width not supported!\n")
        return
    file.write("\n")
  file.write("};\n\n\n")

  file.write("extern const FontInfo FONT_{} = {{\n".format(fname))
  file.write("  .pointSize = {},\n".format(f.pointsize))
  file.write("  .width     = {},\n".format(width))
  file.write("  .height    = {},\n".format(f.height))
  file.write("  .ascent    = {},\n".format(f.ascent))
  file.write("  .inleading = {},\n".format(f.inleading))
  file.write("  .exleading = {},\n".format(f.exleading))
  flags = ["0"]
  if f.italic: flags.append("FONTINFOFLAGS_ITALIC")
  if f.underline: flags.append("FONTINFOFLAGS_UNDERLINE")
  if f.strikeout: flags.append("FONTINFODLAFS_STRIKEOUT")
  file.write("  .flags     = " + " | ".join(flags) + ",\n")
  file.write("  .weight    = {},\n".format(f.weight))
  file.write("  .charset   = {},\n".format(f.charset))
  file.write("  .data      = FONT_{}_DATA,\n".format(fname))
  file.write("  .chptr     = NULL,\n");
  file.write("};\n\n")
  file.write("#else\n\n")
  file.write("extern const FontInfo FONT_{};\n\n".format(fname))
  file.write("#endif\n\n")
  file.write("}\n");


def savefontVar_h(f, file, fname):

  file.write("static const uint8_t FONT_{}_DATA[] = {{\n".format(fname))
  chptr = []
  chptrPos = 0
  for i in range(256):
    width = f.chars[i].width
    chptr.append(chptrPos)
    sz = (int)(1 + f.height * math.ceil(max(8, width) / 8))
    chptrPos += sz
    file.write("  {:2},  ".format(width))
    for j in range(f.height):
      if width == 0:
        v = 0
      else:
        v = f.chars[i].data[j]
      if width <= 8:
        file.write(" 0x{:02x},".format(v << (8 - width)))
      elif width <= 16:
        v = v << (16 - width)
        file.write(" 0x{:02x},".format(v >> 8))
        file.write(" 0x{:02x},".format(v & 0xFF))
      elif width <= 24:
        v = v << (24 - width)
        file.write(" 0x{:02x},".format((v >> 16) & 0xFF))
        file.write(" 0x{:02x},".format((v >> 8) & 0xFF))
        file.write(" 0x{:02x},".format(v & 0xFF))
      else:
        sys.stderr.write("Width not supported!\n")
        return
    file.write("  // {:3} {}\n".format(i, sz))
  file.write("};\n\n\n")

  file.write("static const uint32_t FONT_{}_CHPTR[] = {{\n  ".format(fname))
  col = 1
  for i in chptr:
    file.write("{:4}, ".format(i))
    if col % 15 == 0:
      file.write("\n  ")
    col += 1
  file.write("};\n\n\n")

  file.write("static const FontInfo FONT_{} = {{\n".format(fname))
  file.write("  .pointSize = {},\n".format(f.pointsize))
  file.write("  .width     = 0,\n")
  file.write("  .height    = {},\n".format(f.height))
  file.write("  .ascent    = {},\n".format(f.ascent))
  file.write("  .inleading = {},\n".format(f.inleading))
  file.write("  .exleading = {},\n".format(f.exleading))
  flags = ["0"]
  if f.italic: flags.append("FONTINFOFLAGS_ITALIC")
  if f.underline: flags.append("FONTINFOFLAGS_UNDERLINE")
  if f.strikeout: flags.append("FONTINFODLAFS_STRIKEOUT")
  flags.append("FONTINFOFLAGS_VARWIDTH")
  file.write("  .flags     = " + " | ".join(flags) + ",\n")
  file.write("  .weight    = {},\n".format(f.weight))
  file.write("  .charset   = {},\n".format(f.charset))
  file.write("  .data      = FONT_{}_DATA,\n".format(fname))
  file.write("  .chptr     = FONT_{}_CHPTR,\n".format(fname));
  file.write("};\n\n");
  file.write("}\n");




def savefont(f, file):
  "Write out a .fd form of an internal font description."
  file.write("# .fd font description generated by dewinfont.\n\n")
  file.write("facename " + f.facename + "\n")
  file.write("copyright " + f.copyright + "\n\n")
  #if f.height == f.pointsize: file.write("# ")
  file.write("pointsize " + "%d"%f.pointsize + "\n\n")
  file.write("height " + "%d"%f.height + "\n")
  file.write("ascent " + "%d"%f.ascent + "\n")
  file.write("inleading " + "%d"%f.inleading + "\n")
  file.write("exleading " + "%d"%f.exleading + "\n\n")
  if not f.italic: file.write("# ")
  file.write("italic " + bool(f.italic) + "\n")
  if not f.underline: file.write("# ")
  file.write("underline " + bool(f.underline) + "\n")
  if not f.strikeout: file.write("# ")
  file.write("strikeout " + bool(f.strikeout) + "\n")
  if f.weight == 400: file.write("# ")
  file.write("weight " + "%d"%f.weight + "\n\n")
  if f.charset == 0: file.write("# ")
  file.write("charset " + "%d"%f.charset + "\n\n")
  for i in range(256):
    file.write("char " + "%d"%i + "\nwidth " + "%d"%f.chars[i].width+"\n")
    if f.chars[i].width != 0:
      for j in range(f.height):
        v = f.chars[i].data[j]
        m = 1 << (f.chars[i].width-1)
        for k in range(f.chars[i].width):
          if v & m:
            file.write("x") #"1")
          else:
            file.write(".") #"0")
          v = v << 1
        file.write("\n")
    file.write("\n")



def dofnt(fnt):
  "Create an internal font description from a .FNT-shaped string."
  f = font()
  f.chars = [None] * 256
  version = fromword(fnt[0:])
  ftype = fromword(fnt[0x42:])
  if ftype & 1:
    sys.stderr.write("This font is a vector font\n")
    return None
  off_facename = fromdword(fnt[0x69:])
  if off_facename < 0 or off_facename > len(fnt):
    sys.stderr.write("Face name not contained within font data")
    return None
  f.facename = str(asciz(fnt[off_facename:]), encoding="windows-1252")
  #print "Face name", f.facename
  f.copyright = str(asciz(fnt[6:66] + b"\0"), encoding="windows-1252")
  #print "Copyright", f.copyright
  f.pointsize = fromword(fnt[0x44:])
  #print "Point size", f.pointsize
  f.ascent = fromword(fnt[0x4A:])
  #print "Ascent", f.ascent
  f.inleading = fromword(fnt[0x4C:])
  f.exleading = fromword(fnt[0x4E:])
  f.height = fromword(fnt[0x58:])
  #print "Height", f.height
  f.italic = frombyte(fnt[0x50:]) != 0
  f.underline = frombyte(fnt[0x51:]) != 0
  f.strikeout = frombyte(fnt[0x52:]) != 0
  f.weight = fromword(fnt[0x53:])
  f.charset = frombyte(fnt[0x55:])
  #print "Attrs", f.italic, f.underline, f.strikeout, f.weight
  #print "Charset", f.charset
  # Read the char table.
  if version == 0x200:
    ctstart = 0x76
    ctsize = 4
  else:
    ctstart = 0x94
    ctsize = 6
  maxwidth = 0
  for i in range(256):
    f.chars[i] = char()
    f.chars[i].width = 0
    f.chars[i].data = [0] * f.height
  firstchar = frombyte(fnt[0x5F:])
  lastchar = frombyte(fnt[0x60:])
  for i in range(firstchar,lastchar+1):
    entry = ctstart + ctsize * (i-firstchar)
    w = fromword(fnt[entry:])
    f.chars[i].width = w
    if ctsize == 4:
      off = fromword(fnt[entry+2:])
    else:
      off = fromdword(fnt[entry+2:])
    #print "Char", i, "width", w, "offset", off, "filelen", len(fnt)
    #widthbytes = (w + 7) / 8
    widthbytes = (w + 7) // 8
    for j in range(f.height):
      for k in range(widthbytes):
        bytepos = off + k * f.height + j
        #print bytepos, "->", hex(frombyte(fnt[bytepos:]))
        f.chars[i].data[j] = f.chars[i].data[j] << 8
        f.chars[i].data[j] = f.chars[i].data[j] | frombyte(fnt[bytepos:])
      f.chars[i].data[j] = f.chars[i].data[j] >> (8*widthbytes - w)
  return f

def nefon(fon, neoff):
  "Finish splitting up a NE-format FON file."
  ret = []
  # Find the resource table.
  rtable = fromword(fon[neoff + 0x24:])
  rtable = rtable + neoff
  # Read the shift count out of the resource table.
  shift = fromword(fon[rtable:])
  # Now loop over the rest of the resource table.
  p = rtable+2
  while 1:
    rtype = fromword(fon[p:])
    if rtype == 0:
      break  # end of resource table
    count = fromword(fon[p+2:])
    p = p + 8  # type, count, 4 bytes reserved
    for i in range(count):
      start = fromword(fon[p:]) << shift
      size = fromword(fon[p+2:]) << shift
      if start < 0 or size < 0 or start+size > len(fon):
        sys.stderr.write("Resource overruns file boundaries\n")
        return None
      if rtype == 0x8008: # this is an actual font
        #print "Font at", start, "size", size
        font = dofnt(fon[start:start+size])
        if font == None:
          sys.stderr.write("Failed to read font resource at %x" \
          % start)
          return None
        ret = ret + [font]
      p = p + 12 # start, size, flags, name/id, 4 bytes reserved
  return ret

def pefon(fon, peoff):
  "Finish splitting up a PE-format FON file."
  dirtables=[]
  dataentries=[]
  def gotoffset(off,dirtables=dirtables,dataentries=dataentries):
    if off & 0x80000000:
      off = off &~ 0x80000000
      dirtables.append(off)
    else:
      dataentries.append(off)
  def dodirtable(rsrc, off, rtype, gotoffset=gotoffset):
    number = fromword(rsrc[off+12:]) + fromword(rsrc[off+14:])
    for i in range(number):
      entry = off + 16 + 8*i
      thetype = fromdword(rsrc[entry:])
      theoff = fromdword(rsrc[entry+4:])
      if rtype == -1 or rtype == thetype:
        gotoffset(theoff)

  # We could try finding the Resource Table entry in the Optional
  # Header, but it talks about RVAs instead of file offsets, so
  # it's probably easiest just to go straight to the section table.
  # So let's find the size of the Optional Header, which we can
  # then skip over to find the section table.
  secentries = fromword(fon[peoff+0x06:])
  sectable = peoff + 0x18 + fromword(fon[peoff+0x14:])
  for i in range(secentries):
    secentry = sectable + i * 0x28
    secname = asciz(fon[secentry:secentry+8])
    secrva = fromdword(fon[secentry+0x0C:])
    secsize = fromdword(fon[secentry+0x10:])
    secptr = fromdword(fon[secentry+0x14:])
    if secname == b".rsrc":
      break
  if secname != b".rsrc":
    sys.stderr.write("Unable to locate resource section\n")
    return None
  # Now we've found the resource section, let's throw away the rest.
  rsrc = fon[secptr:secptr+secsize]

  # Now the fun begins. To start with, we must find the initial
  # Resource Directory Table and look up type 0x08 (font) in it.
  # If it yields another Resource Directory Table, we stick the
  # address of that on a list. If it gives a Data Entry, we put
  # that in another list.
  dodirtable(rsrc, 0, 0x08)
  # Now process Resource Directory Tables until no more remain
  # in the list. For each of these tables, we accept _all_ entries
  # in it, and if they point to subtables we stick the subtables in
  # the list, and if they point to Data Entries we put those in
  # the other list.
  while len(dirtables) > 0:
    table = dirtables[0]
    del dirtables[0]
    dodirtable(rsrc, table, -1) # accept all entries
  # Now we should be left with Resource Data Entries. Each of these
  # describes a font.
  ret = []
  for off in dataentries:
    rva = fromdword(rsrc[off:])
    start = rva - secrva
    size = fromdword(rsrc[off+4:])
    font = dofnt(rsrc[start:start+size])
    if font == None:
      sys.stderr.write("Failed to read font resource at %x" % start)
      return None
    ret = ret + [font]
  return ret

def dofon(fon):
  "Split a .FON up into .FNTs and pass each to dofnt."
  # Check the MZ header.
  if fon[0:2] != b"MZ":
    sys.stderr.write("MZ signature not found\n")
    return None
  # Find the NE header.
  neoff = fromdword(fon[0x3C:])
  if fon[neoff:neoff+2] == b"NE":
    return nefon(fon, neoff)
  elif fon[neoff:neoff+4] == b"PE\0\0":
    return pefon(fon, neoff)
  else:
    sys.stderr.write("NE or PE signature not found\n")
    return None

def isfon(data):
  "Determine if a file is a .FON or a .FNT format font."
  if data[0:2] == b"MZ":
    return 1 # FON
  else:
    return 0 # FNT


if sys.version_info[0] < 3:
    sys.stderr.write("\nPlease execute with Python 3!\n\n")
    exit()

a = sys.argv[1:]
options = 1
outfile = None
prefix = None
infile = None
outext = ".fd"
if len(a) == 0:
  print("usage: dewinfont [-h] [-o outfile | -p prefix] file")
  sys.exit(0)
while len(a) > 0:
  if a[0] == "--":
    options = 0
    a = a[1:]
  elif options and a[0][0:1] == "-":
    if a[0] == "-o":
      try:
        outfile = a[1]
        a = a[2:]
      except IndexError:
        sys.stderr.write("option -o requires an argument\n")
        sys.exit(1)
    elif a[0] == "-p":
      try:
        prefix = a[1]
        a = a[2:]
      except IndexError:
        sys.stderr.write("option -p requires an argument\n")
        sys.exit(1)
    elif a[0] == "-h":
      outext = ".h"
      a = a[1:]
    else:
      sys.stderr.write("ignoring unrecognised option "+a[0]+"\n")
      a = a[1:]
  else:
    if infile != None:
      sys.stderr.write("one input file at once, please\n")
      sys.exit(1)
    infile = a[0]
    a = a[1:]

print(infile)
fp = open(infile, "rb")
data = fp.read()
fp.close()

if isfon(data):
  fonts = dofon(data)
else:
  fonts = [dofnt(data)]

if outfile == None:
  outfile = os.path.splitext(os.path.basename(infile))[0] + outext

for i in range(len(fonts)):
  if prefix != None:
    fname = prefix + "%02d"%i + outext
  else:
    if len(fonts) > 1:
      fname = os.path.splitext(outfile)[0] + "%02d"%i + os.path.splitext(outfile)[1]
    else:
      fname = outfile
  fp = open(fname, "w")
  savefont_h(fonts[i], fp) if outext == ".h" else savefont(fonts[i], fp)
  fp.close()
